ECE 5725 Fall 2022
Eric Moon and Vinay Bhamidipati
15 December 2022
Imagine fresh tomatoes, fresh herbs–any plant you can imagine, homegrown with just a press of a button. What if you could take a beautiful home garden and show it off inside the house? Ivyponic takes all of the difficulty and micromanagement out of home gardening while simultaneously being a beautiful, elegant showpiece for the house.
During the semester, our team (Vinay Bhamidipati and Eric Moon) built Ivyponic. Ivyponic is a sealed, hydroponic indoor gardening solution that can automatically grow a plant under customizable conditions, including airflow, lighting, temperature, humidity, and nutrient density. Ivyponic grows plants without the need to micromanage or take care of the plants directly. We used the PiTFT screen to allow for customization of these conditions to optimize for a specific plant’s needs. We created an hydroponic growing system, using a pump to deliver fertilized water to plants, without the need of soil. We used the RPi to manage all variables for plant growth to maintain user-defined targets. We used PPM, temperature, and humidity sensors to gather the necessary environmental data.
Inital Sketch of Ivyponic
Final Picture of Ivyponic
Each phase of design for Ivyponic began with a drawing. During the ideation phase of Ivyponic, we envisioned the system as depicted in the drawing above. The Raspberry Pi and PiTFT would be mounted at the bottom of the enclosure, displaying temperature, humidity, airflow, and lighting parameters. Above the Raspberry Pi, there would be a catch pan that would route excess drip-off back into the water pump. The water reservoir would be located at the top of the enclosure, and the pump would have two valve-controlled spray heads–one for misting the enclosure and one for watering the plants. The system would be complete with humidity, temperature, and airflow sensors.
In the following phases, we dug deeper into the design specifics, using this initial drawing as a starting point. One constant in our design process was the chassis of Ivyponic. Because we wanted to design a home garden product that would look visually distinct and appealing, we decided to use clear acrylic. This allowed the plant and lighting to be a focal point of the aesthetics, while also allowing users to see the behind-the-scenes of the device; the electronics, wiring, and pump system were fully visible.
Sketch of Interactive Menus
Status Menu
Targets menu
Lighting menu
Presets menu
Another key point of design was the user interface on the PiTFT. Before coding the Raspberry Pi interface, we planned out each specific menu. At the top of each menu is a taskbar that displays the currently active menu. Additionally, there are forward, backward, and power icons that illustrate the functions of the adjacent buttons. The “Status” menu would display the current status of the enclosure including the current humidity, temperature, PPM, fan speed, and RGB values. These values would be retrieved from the various sensors, the fan, and the LED light strip, respectively. The next screen would allow the user to adjust the user-controlled targets to their desired values. For example, if the user wanted to maintain a temperature of 25 degrees celsius in the enclosure, he or she could set that from this menu. Next, the “Lighting” menu would allow the user to toggle the lights on and off for the various hours in the day. Finally, the “Presets'' menu would allow the user to select from hard-coded presets. Each preset contains the optimal values for temperature, humidity, PPM, and RGB for a specific plant. Upon selecting one of these presets, these optimal values would be set by the system, and the enclosure would attempt to maintain them.
Final Sketch of Ivyponic
Concurrently while coding the interface, we planned out the construction of the actual enclosure. After realizing that our pump would not be able to activate the misters, we had to pivot to a drip-irrigation system as shown above. Once we came up with this design, building it out was relatively easy, even though we expected the physical construction to be the hardest part.
Targets select menu
Lighting select menu
Presets select menu
In our original plans for Ivyponic, the display would be touchscreen, which influenced how we envisioned the user would interact with the device. However, upon implementing the menu interfaces, we discovered that our PiTFT touchscreen was non-functional, reporting random values for touch positions. Because of this, we had to redesign the display, coming up with UI design that did not require touch functionality. Eventually, we settled on the above system where each screen would have a select button that brought up a nested menu with new button functions. These functions would change based on which menu was being displayed. In the case of the first menu, the select menu was unnecessary, as the menu was for viewing status, not editing values. In the subsequent menus—targets, lighting, and presets—a select button was added. This would switch the current tab to a ‘selection mode’ where the side buttons functionality changed to allow for users to customize values. This would include a button to cycle through parameters and buttons to either toggle or increment values. In the place of a power button, a back button was added, to return to the menu to ‘viewing mode’.
To aid with testing and in accordance with good coding practices, each module was factored out into its own file. This made testing the different sensors, the pump, the fan, and the LEDs relatively easy. First, we would test each component individually within its respective file under the “if __name__ == ‘__main__’:” line. Then, if it functioned correctly, we would simply instantiate an object of it in the main file. Since both the pump and the fan were driven from the motor controller, we instantiated them both as Motor objects and tested them within the motor.py file.
This testing method made it easy to narrow down whether the problems we had were caused by the individual components or rather the main code operation. One specific problem we ran into was that the fan and pump were not functioning properly within the main code. Because we had already tested them in motor.py, we knew it had to be a problem in main.py. We discovered that, because we were instantiating the fan and pump on new threads, eventually a thread would not have GPIO.BCM set which would cause the program to crash. To fix this problem, we modified the main code to only spawn one thread each for the fan and pump.
To diagnose hardware problems, before we ever ran code, we tested our hardware with benchtop EE tools including power supplies and oscilloscopes. Using this method, we determined that there were no hardware problems with the pump and fan we chose to use. Furthermore, we were able to determine that certain GPIO pins that we were using on our Raspberry Pi were dysfunctional.
One fundamental challenge we ran into was that the pressure our pump could generate was not sufficient to activate the misters that we had planned to use in our original design. This forced us to pivot to a drip-irrigation system that did not rely on sprayers. Although we considered using alternative pumps that could activate the sprayers, ultimately we decided not to use them due to time and budget constraints.
Challenges also arose in the implementation of the many sensors, motors, and lights all on the Raspberry Pi. Due to the large number of devices and the presence of several fried GPIO pins, wiring up the sensors was not easy. First, the PiTFT already uses many of the GPIO pins, and uses the SPI0 channel. To enable additional channels, /boot/config.txt was edited, but strange PiTFT behavior meant that certain SPI channels couldnt be used, or rebooting an indefinite number of times was required to eventually see SPI channels become enabled. The NeoPixel light strips also could only use specific GPIO pins, leading to many of the GPIO pins being shifted around, until all devices, specifically the PPM sensor (SPI) and temperature/humidity sensor (I2C), were all able to run correctly. After demoing with the temperature sensor, the I2C connection no longer appears stable, and running i2cdetect reveals that the connection seems to ‘flicker’, with Linux constantly detecting and losing connection to the AHT20.
As mentioned, the challenges of the underpowered pump led to the redesign of the watering system. However, with the new drip watering system was equally effective. The final design worked as intended, with live updates of PPM, humidity, temperature, and lighting conditions being sent to the Raspberry Pi. Additionally, the gardening control system worked as expected, with notifications being displayed for high or low PPM/humidity, and fan speeds and lighting being appropriately adjusted based on temperature and light color/scheduling. One issue that appeared in the final presentation was the reading of the PPM value. In testing the PPM sensor, only solutions with PPM value less than 310 PPM were used in testing. However in the demo, a large amount of fertilizer was poured into the nutrient solution, resulting in a PPM maxing out at around 550 PPM. Addition of more fertilizer to the water did not affect PPM—the sensor could not read any larger values. In the future, this would need to be addressed, as many hydroponic systems perform optimally at PPM values up to 700-800. Additionally, issues with the AHT20 temperature and humidity sensor presented after the final demo. After successfully demoing the AHT20, the next day the temperature sensor seemed to have failed. As mentioned in the challenges section, it no longer can consistently communicate via I2C for more than a few seconds. Both these issues are fairly minor. A larger range PPM sensor and swapping the AHT20 for a new one would be simple solutions.
Overall, Ivyponic was a success—an automatic gardening solution powered by the Raspberry Pi. Even after a few days, our withered green onions looked rejuvenated.
In conclusion, Ivyponic is a mostly-automated gardening system, which demonstrates the potential to become fully automatic by improving cooling/airflow, automatic PPM balancing, and a sensor for root hydration. Designing and building Ivyponic led us to realize the true difficulty and complexity in building a system like this. How can we ensure the specific plant is getting the correct amount of water? How can ensure the plant gets enough oxygen while regulating temperature and humidity? These were all questions that were only partially answered in our final design; to fully implement our vision of Ivyponic, more sensors, electronics, testing, and data would be needed. With that being said, the process of designing and iterating on that design to build a final project taught us a lot about embedded systems and general product/system design. Adapting to issues like the underpowered pump and fried GPIO pins were critical to deliver the final demo, and helped us learn to think creatively and non-linearly.
Software wise, there were a lot of things we would have loved to implement given extra time. For the presets menu, we would explore grabbing presets from an agricultural database. We would explore finer-grain control of the lighting, including options to have different color spectra for the different phases of a plant’s growth (e.g. flowering vs. vegetative) since each phase has slightly different optimal spectra needs. This data would be included in the presets. Furthermore, we would explore gradual dimming and brightening of the lights rather than the current binary implementation of either full or zero brightness. Hardware wise, we would explore implementing a moisture sensor within the growing medium to regulate the pump watering interval. We would redesign the enclosure such that it conceals the wiring that takes away from the beauty of Ivyponic. We would have routed the electrical connections outside of a non-removable panel, rather than the removable one. We would have implemented a mechanical system that would automatically dispense fertilizer, rather than requiring the user to enter fertilizer. Finally, we would have used a more robust growing medium since the one we used was fragile when wet.
esm234@cornell.edu
Sensor Implementation | Chassis Construction | GUI
vkb29@cornell.edu
Motor Implementation | Control System | GUI
Purchased:
From Lab Kit:
From Lab or Elsewhere:
#main.py from dataclasses import dataclass import RPi.GPIO as GPIO import os import pygame from pygame.locals import * import numpy from time import perf_counter, sleep from datetime import datetime from sys import argv from collections import deque from threading import Thread from PPM import PPM import AHT20 from collections import deque import sys from motor import Motor from LED import LED from graphics import Graphics print(sys.executable) FULL_SPEED = 100 HALF_SPEED = 50 # GPIO pins for the A and B motors AIN1 = 6 PUMP_PWM = 4 BIN1 = 16 FAN_PWM = 24 ''' @dataclass class Graphics: # Colors WHITE = 255, 255, 255 BLACK = 0, 0, 0 RED = 255, 0, 0 GREEN = 200, 90, 10 BLUE = 0, 0, 255 ORANGE = 234, 163, 50 BLUISH = 45, 175, 220 GREENISH = 45, 220, 110 # Fonts menu_button_font = None display_font = None # Screen variables screen = None def initialize_fonts(self): self.menu_button_font = pygame.font.Font(None, 20) self.display_font = pygame.font.Font(None, 30) ''' @dataclass class Presets: TOMATO = {'temp': 21, 'humidity': 75, 'ppm': 700, 'r': 255, 'g': 0, 'b': 255} CILANTRO = {'temp': 19, 'humidity': 30, 'ppm': 500, 'r': 100, 'g': 100, 'b': 100} BASIL = {'temp': 25, 'humidity': 50, 'ppm': 700, 'r': 140, 'g': 0, 'b': 255} POTATO = {'temp': 18, 'humidity': 50, 'ppm': 700, 'r': 200, 'g': 20, 'b': 140} class Interface: pos = None selected: deque = deque() def update(self): self.pos = None class Interface1(Interface): pass class Interface2(Interface): selected: deque[int] = deque([1,0,0,0,0,0]) keys: list[str] = ["humidity", "temp", "ppm", "r", "g", "b"] blink: int = 0 def update(self): super().update() class Interface3(Interface): AM: bool = True selected: deque[int] = deque([1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]) class Interface4(Interface): selected = deque([1,0,0,0]) presets = [Presets.CILANTRO,Presets.POTATO, Presets.BASIL, Presets.TOMATO] @dataclass class State: # storing current values of environmental parameters humidity: float = 0 temperature: float = 0 rpm: float = 0 ppm: float = 0 # targets targets = {"humidity": 0.0, "temp": 0.0, "ppm": 0.0, "r": 0.0, "g": 0.0, "b": 0.0} increments = {"humidity": 1, "temp": 1, "ppm": 10, "r": 3, "g": 3, "b": 3} # parameters to store for LED light_schedule = [0]*24 # menu handling menu: int = 1 interface = Interface() select = False t0 = perf_counter() def __init__(self): self.PPM_sensor = PPM() self.AHT20 = AHT20.AHT20(BusNum=1) self.controller:Controller = None # updating commands def update(self): self.humidity = self.AHT20.get_humidity() # read_humidity self.temperature = self.AHT20.get_temperature() # read_temperature self.rpm = self.controller.fan_speed #not actually rpm, actually duty cycle (%) of fan ppm_val = self.PPM_sensor.read() self.ppm = ppm_val if ppm_val != -1 else self.ppm # read_ppm def increment_target(self, target, multiplier): increment = self.increments[target] new_val = self.targets[target] + multiplier*increment if target == "humidity": if new_val < 10: new_val = 10 elif new_val > 100: new_val = 100 elif target == "temp": if new_val > 28: new_val = 28 elif new_val < 16: new_val = 16 elif target == "ppm": if new_val > 1200: new_val = 1200 elif new_val < 350: new_val = 350 elif target in {"r", "g", "b"}: if new_val > 255: new_val = 255 elif new_val < 0: new_val = 0 self.targets[target] = new_val if target in {"r", "g", "b"}: color = self.targets['r'], self.targets['g'], self.targets['b'] self.controller.update_LED(color) class Controller: #motors #PUMP = motor.Motor("Pump", PUMP_PWM, AIN1) #FAN = motor.Motor("Fan", FAN_PWM, BIN1) #parameters fan_speed = 0 #duty cycle (%) for fan control = False def __init__(self, STATE): self.STATE:State = STATE self.STATE.controller = self color = self.STATE.targets["r"], self.STATE.targets["g"], self.STATE.targets["b"] self.LED_STRIP = LED(color) self.PUMP = Motor("Pump", PUMP_PWM, AIN1, offset=-1) self.FAN = Motor("Fan", FAN_PWM, BIN1) def start(self): Controller.control = True #create threads fan_thread = Thread(target=self.control_fan, daemon=True) pump_thread = Thread(target=self.control_pump, daemon=True) LED_thread = Thread(target=self.control_LED, daemon=True) #start threads fan_thread.start() pump_thread.start() LED_thread.start() def stop(self): Controller.control = False def control_fan(self): #drive fan indefinitely self.FAN.drive(self.fan_speed,-1) while(Controller.control): target_temp = self.STATE.targets["temp"] if self.STATE.temperature < target_temp: #decrease fan speed self.update_fan_speed(-1) elif self.STATE.temperature > target_temp: #increase fan speed self.update_fan_speed(1) sleep(3) def control_pump(self): #drive pump indefinitely self.PUMP.drive(0, -1) while(Controller.control): #run pump at 100% for 30 seconds self.PUMP.setSpeed(100) sleep(30) #stop pump for 90 seconds self.PUMP.setSpeed(0) sleep(90) def control_LED(self): while(Controller.control): time = datetime.now().time() time_index = time.hour - 1 color: tuple[float, float, float] = self.STATE.targets['r'], self.STATE.targets['g'], self.STATE.targets['b'] self.LED_STRIP.color = color print(f"Time: {time}") if(self.STATE.light_schedule[time_index]): print("LEDs: Turning on lights") self.LED_STRIP.turnon() else: print("LEDs: Shutting off lights") self.LED_STRIP.shutoff() sleep(60) def update_fan_speed(self, multiplier): '''multiplier = 1 or -1 ''' increment = 10 * multiplier new_speed = self.fan_speed + increment if new_speed > 100: new_speed = 100 elif new_speed < 0: new_speed = 0 self.fan_speed = new_speed self.FAN.setSpeed(new_speed) def update_LED(self, color): self.LED_STRIP.color = color time = datetime.now().time() time_index = time_index = time.hour - 1 if(self.STATE.light_schedule[time_index]): self.LED_STRIP.fill(color) def b17_callback(channel): ''' Moves menu forward Performs 1st button function in inner menu ''' print("FWD") if not STATE.select: if STATE.menu < 4: STATE.menu += 1 STATE.interface = create_interface(STATE.menu) else: STATE.menu = 1 else: if(STATE.menu == 2): #cycle forward STATE.interface.selected.rotate() elif(STATE.menu == 3): #cycle through STATE.interface.selected.rotate() elif(STATE.menu == 4): #cycle forward STATE.interface.selected.rotate() def b22_callback(channel): ''' Moves menu backward in outer menu Performs 2nd button function in inner menu ''' print("BACK") if not STATE.select: if STATE.menu > 1: STATE.menu -= 1 STATE.interface = create_interface(STATE.menu) else: STATE.menu = 4 else: if(STATE.menu == 2): #increment ind = STATE.interface.selected.index(1) targ = STATE.interface.keys[ind] STATE.increment_target(targ, 1) pass elif(STATE.menu == 3): #cycle backward STATE.interface.selected.rotate(-1) pass elif(STATE.menu == 4): #cycle backward STATE.interface.selected.rotate(-1) pass def b23_callback(channel): """Toggles select in outer menu 3rd button function in inner menu """ if STATE.menu in {2,3,4}: if not STATE.select: STATE.select = True elif(STATE.menu == 2): #decrement ind = STATE.interface.selected.index(1) targ = STATE.interface.keys[ind] STATE.increment_target(targ, -1) pass elif(STATE.menu == 3): #toggle on and off ind = STATE.interface.selected.index(1) STATE.light_schedule[ind] = 0 if STATE.light_schedule[ind] else 1 pass elif(STATE.menu == 4): #enter ind = STATE.interface.selected.index(1) STATE.targets = STATE.interface.presets[ind] color = STATE.targets['r'], STATE.targets['g'], STATE.targets['b'] STATE.controller.update_LED(color) def b27_callback(channel): ''' Powers off in outer menu Exits select menu in inner menu ''' if not STATE.select: shutdown() else: STATE.select = False def startup(): ''' To be called at the start of the program Sets up GPIO inputs and attaches their callbacks ''' GPIO.setmode(GPIO.BCM) GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.add_event_detect( 17, GPIO.FALLING, callback=b17_callback, bouncetime=300) GPIO.add_event_detect( 22, GPIO.FALLING, callback=b22_callback, bouncetime=300) GPIO.add_event_detect( 23, GPIO.FALLING, callback=b23_callback, bouncetime=300) GPIO.add_event_detect( 27, GPIO.FALLING, callback=b27_callback, bouncetime=300) def shutdown(): ''' Function to be called whenever a program ends Cleans up GPIO and sets code_run to false. Exits Python. ''' print("Shutting down...") global code_run code_run = False def outputs(pin_list): '''Sets up the pin numbers in pin_list as GPIO outputs ''' for pin in pin_list: GPIO.setup(pin, GPIO.OUT) def inputs(pin_list): '''Sets up the pin numbers in pin_list as GPIO outputs ''' for pin in pin_list: GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) def distance(p1, p2): return ((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)**(1/2) def draw_text(position,text,color, corner="center", font="menu_button"): if font == "menu_button": text_surface = GRAPHICS.menu_button_font.render(text, True, color) elif font == "small_text": text_surface = GRAPHICS.small_font.render(text, True, color) elif font == "display": text_surface = GRAPHICS.display_font.render(text, True, color) if corner == "center": rect = text_surface.get_rect(center=position) elif corner == "topleft": rect = text_surface.get_rect(topleft=position) GRAPHICS.screen.blit(text_surface, rect) def draw_arrow_down(point,color): arrow_top = (point[0],point[1]-9) arrow_left = (point[0]-3,point[1]-3) arrow_right = (point[0]+3,point[1]-3) pygame.draw.line(GRAPHICS.screen, GRAPHICS.RED, point, arrow_top, 2) pygame.draw.line(GRAPHICS.screen, GRAPHICS.RED, point, arrow_left, 2) pygame.draw.line(GRAPHICS.screen, GRAPHICS.RED, point, arrow_right, 2) def draw_arrow(point,color): arrow_top = (point[0],point[1]+9) arrow_left = (point[0]-3,point[1]+3) arrow_right = (point[0]+3,point[1]+3) pygame.draw.line(GRAPHICS.screen, GRAPHICS.RED, point, arrow_top, 2) pygame.draw.line(GRAPHICS.screen, GRAPHICS.RED, point, arrow_left, 2) pygame.draw.line(GRAPHICS.screen, GRAPHICS.RED, point, arrow_right, 2) def collidepoint(rect, pt): """rect - (x topleft, y topleft, width, height) pt - (x,y) """ xtopleft, ytopleft, width, height = rect x,y = pt collide = False if x >= xtopleft and x <= xtopleft + width and y >= ytopleft and y <= ytopleft + height: collide = True return collide def menu(fwd=True, back=True, power=True): """Super class menu Draws top menu display, forward, back buttons, and power """ # highlight appropriate tab left = 80*STATE.menu - 78 pygame.draw.rect(GRAPHICS.screen, GRAPHICS.GREEN, (left, 2, 78, 27), 0) if not STATE.select: # draw button text draw = [fwd, back, power] button_texts = ["->", "<-", "PWR"] button_positions = [(300, 49), (300, 111), (300, 227)] for d, text, pos in zip(draw, button_texts, button_positions): if d: text_surface = GRAPHICS.menu_button_font.render(text, True, GRAPHICS.WHITE) rect = text_surface.get_rect(center=pos) GRAPHICS.screen.blit(text_surface, rect) if STATE.menu in {2,3,4}: text_surface = GRAPHICS.menu_button_font.render("select", True, GRAPHICS.WHITE) rect = text_surface.get_rect(center = (296, 170)) GRAPHICS.screen.blit(text_surface, rect) # draw top menu text menu_texts = ["Status", "Targets", "Lighting", "Presets"] menu_positions = [(40, 15), (120, 15), (200, 15), (280, 15)] for text, pos in zip(menu_texts, menu_positions): text_surface = GRAPHICS.menu_button_font.render(text, True, GRAPHICS.WHITE) rect = text_surface.get_rect(center=pos) GRAPHICS.screen.blit(text_surface, rect) # draw top menu lines/boxes pygame.draw.rect(GRAPHICS.screen, GRAPHICS.BLUISH, (0, 0, 320, 30), 2) pygame.draw.line(GRAPHICS.screen, GRAPHICS.BLUISH, (80, 30), (80, 0), 2) pygame.draw.line(GRAPHICS.screen, GRAPHICS.BLUISH, (160, 30), (160, 0), 2) pygame.draw.line(GRAPHICS.screen, GRAPHICS.BLUISH, (240, 30), (240, 0), 2) def menu1(): """Draws the current status """ menu() texts = [f"Humidity: {STATE.humidity:.01f}%", f"Temp: {STATE.temperature:.01f}C", f"PPM: {STATE.ppm:.01f} ppm", f"Fan Speed: {STATE.rpm:.01f}%", f'R: {round(STATE.targets["r"] *100/ 255, 1)}% G: {round(STATE.targets["g"] *100/255, 1)}% B: {round(STATE.targets["b"] *100/255, 1)}%'] positions = [(10, 50), (10, 90), (10, 130), (10, 170), (10, 210)] for text, pos in zip(texts, positions): draw_text(pos, text, GRAPHICS.WHITE, corner="topleft", font="display") if STATE.humidity < STATE.targets['humidity']: draw_text((170, 60), "(LOW: MIST ENCLOSURE!)", GRAPHICS.RED, corner="topleft", font="small_text") if STATE.ppm < STATE.targets['ppm']: draw_text((170, 140), "(LOW: ADD FERTILIZER!)", GRAPHICS.RED, corner="topleft", font="small_text") def menu2(): """Targets Set desired targets that the embedded system will attempt to maintain """ humidity_pos = (10, 50, 150, 20) temp_pos =(10, 90, 120, 20) ppm_pos =(10, 130, 120, 20) menu() texts = [f"Humidity: {STATE.targets['humidity']:.01f}%", f"Temp: {STATE.targets['temp']:.01f}F", f"PPM: {STATE.targets['ppm']:.01f} ppm", f"R: {STATE.targets['r'] *100/ 255:.1f}%" ,f"G: {STATE.targets['g'] *100/255:.1f}%",f"B: {STATE.targets['b'] *100/255:.1f}%"] positions = [(10, 50), (10, 100), (10, 150), (10, 200), (100,200),(190,200)] #selected = [t == STATE.interface.selected for t in STATE.targets.keys()] if STATE.select: for text, loc, s in zip(texts, positions, STATE.interface.selected): text_surface = GRAPHICS.display_font.render( text, True, GRAPHICS.BLUE if s else GRAPHICS.WHITE) # highlight rect = text_surface.get_rect(topleft=loc) GRAPHICS.screen.blit(text_surface, rect) button_positions = [(295, 49), (281, 111), (281, 170), (297, 227)] button_texts = ["cycle", "increment", "decrement", "back"] for p, txt in zip(button_positions, button_texts): draw_text(p, txt, GRAPHICS.WHITE) else: for text, loc in zip(texts, positions): text_surface = GRAPHICS.display_font.render(text, True, GRAPHICS.WHITE) # highlight rect = text_surface.get_rect(topleft=loc) GRAPHICS.screen.blit(text_surface, rect) ''' if STATE.interface.selected: pygame.draw.polygon(GRAPHICS.screen, GRAPHICS.ORANGE, ((200, 110), (210, 100), (220, 110))) # up arrow pygame.draw.polygon(GRAPHICS.screen, GRAPHICS.ORANGE, (( 200, 130), (210, 140), (220, 130))) # down arrow ''' def menu3(): """Lighting View current lighting settings, change lighting schedule """ menu() #print menu top label and buttons schedule = STATE.light_schedule if(STATE.select): #draw menu3 select screen button indicators draw_text(position=(300,49),text='>>',color=GRAPHICS.WHITE) draw_text(position=(300,111),text='<<',color=GRAPHICS.WHITE) draw_text(position=(295,170),text='toggle',color=GRAPHICS.WHITE) draw_text(position=(295,227),text='back',color=GRAPHICS.WHITE) #draw PM/AM title draw_text((60,70),'AM Schedule',color=GRAPHICS.WHITE) draw_text((60,150),'PM Schedule',color=GRAPHICS.WHITE) #draw schedule graphic l1 = (20, 110, 260, 110) l2 = (20, 190, 260, 190) pygame.draw.line(GRAPHICS.screen, GRAPHICS.GREEN, l1[0:2], l1[2:4], 3) pygame.draw.line(GRAPHICS.screen, GRAPHICS.GREEN, l2[0:2], l2[2:4], 3) lpoint = (40, 110, 190) for i in range(11): m=2 if i % 2 == 0 else 3 pygame.draw.line(GRAPHICS.screen, GRAPHICS.GREEN, (lpoint[0]+i*20, lpoint[1]), (lpoint[0]+i*20, lpoint[1]-10*m),2) pygame.draw.line(GRAPHICS.screen, GRAPHICS.GREEN, (lpoint[0]+i*20, lpoint[2]), (lpoint[0]+i*20, lpoint[2]-10*m),2) #draw lighting timeslots on graphic top_left = [22,94] width_height = [18,15] for i, val in enumerate(schedule): if i == 12: top_left = [22,174] if val == 1: pygame.draw.rect(GRAPHICS.screen, GRAPHICS.GREENISH, top_left+width_height, 0) top_left[0]+=20 #if select menu is chosen if(STATE.select): current = STATE.interface.selected.index(1) arrow_point = (30+20*current,120) if current < 12 else (30+20*(current-12),200) draw_arrow(arrow_point,color=GRAPHICS.RED) def menu4(): """Presets """ menu() x_init = 10 y_init = 35 width = 100 spacing = 5 rect1 = (x_init, y_init, width, width) rect2 = (x_init + width + spacing, y_init, width, width) rect3 = (x_init, y_init + width + spacing, width, width) rect4 = (x_init + width + spacing, y_init + width + spacing, width, width) texts = ['Cilantro', 'Potato', 'Basil', 'Tomato'] positions = [(rect1[0]+3, rect2[1]+2), (rect2[0]+3, rect2[1]+2), (rect3[0]+3, rect3[1]+2), (rect4[0]+3, rect4[1]+2)] pygame.draw.rect(GRAPHICS.screen, GRAPHICS.ORANGE, rect1, 3) pygame.draw.rect(GRAPHICS.screen, GRAPHICS.ORANGE, rect2, 3) pygame.draw.rect(GRAPHICS.screen, GRAPHICS.ORANGE, rect3, 3) pygame.draw.rect(GRAPHICS.screen, GRAPHICS.ORANGE, rect4, 3) #draw presets targets presets = [Presets.CILANTRO, Presets.POTATO, Presets.BASIL, Presets.TOMATO] for preset, name, pos in zip(presets, texts, positions): draw_text((pos[0]+10,pos[1]+20),f'Temp: {preset["temp"]}', color=GRAPHICS.BLUISH, corner="topleft", font="small_text") draw_text((pos[0]+10,pos[1]+32),f'Humidity: {preset["humidity"]}', color=GRAPHICS.BLUISH, corner="topleft", font="small_text") draw_text((pos[0]+10,pos[1]+44),f'PPM: {preset["ppm"]}', color=GRAPHICS.BLUISH, corner="topleft", font="small_text") draw_text((pos[0]+10,pos[1]+56),f'{color_to_percent(preset["r"], "R")}', color=GRAPHICS.BLUISH, corner="topleft", font="small_text") draw_text((pos[0]+10,pos[1]+68),f'{color_to_percent(preset["g"], "G")}', color=GRAPHICS.BLUISH, corner="topleft", font="small_text") draw_text((pos[0]+10,pos[1]+80),f'{color_to_percent(preset["b"],"B")}', color=GRAPHICS.BLUISH, corner="topleft", font="small_text") if not STATE.select: for text, loc in zip(texts, positions): draw_text(loc, text, GRAPHICS.WHITE, corner="topleft") else: for text, loc, s in zip(texts, positions, STATE.interface.selected): draw_text(loc, text, GRAPHICS.BLUE if s else GRAPHICS.WHITE, corner="topleft") if STATE.select: button_positions = [(300, 49), (300, 111), (300, 170), (300, 227)] button_texts = ["->", "<-", "enter", "back"] for p, txt in zip(button_positions, button_texts): draw_text(p, txt, GRAPHICS.WHITE) # state variable to keep track of motor history STATE = State() # graphics variable to store data about graphics GRAPHICS = Graphics() #Timeout time TIMEOUT = 300 def get_color_string(r, g, b): return f'R: {round(r *100/ 255, 1)}% G: {round(g*100/255, 1)}% B: {round(b*100/255, 1)}%' return f'{round(r *100/ 255, 1)}%', f'G: {round(g*100/255, 1)}%', f'B: {round(b*100/255, 1)}%' def color_to_percent(color_val, color_str): return f'{color_str}: {round(color_val *100/ 255, 1)}%' def main(): startup() # initialize gpio pins global code_run code_run = True # setting up a global timeout t0 = perf_counter() # setting up GPIO inputs and outputs, pwm signals inputs([]) outputs([]) # read initial parameters: with open("targets.txt", 'r') as f: lines = [l.strip().split() for l in f.readlines()] STATE.targets = {l[0]: float(l[1]) for l in lines} print(lines) print(STATE.targets) with open("lighting.txt", 'r') as f: STATE.light_schedule = [int(b) for b in f.readline().strip()] # START OF GAME LOOP # pygame setup os.putenv('SDL_VIDEODRIVER', 'fbcon') # Display on piTFT os.putenv('SDL_FBDEV', '/dev/fb0') os.putenv('SDL_MOUSEDRV', 'TSLIB') # Track mouse clicks on piTFT os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen') pygame.init() size = width, height = 320, 240 clk = pygame.time.Clock() screen = pygame.display.set_mode(size) pygame.mouse.set_visible(False) GRAPHICS.set_screen(screen) GRAPHICS.initialize_fonts() screen.fill(GRAPHICS.BLACK) # erase workspace pygame.display.flip() controller = Controller(STATE) controller.start() while(code_run is True and perf_counter() - t0 < TIMEOUT): screen.fill(GRAPHICS.BLACK) # erase workspace STATE.update() if STATE.menu == 1: menu1() elif STATE.menu == 2: menu2() elif STATE.menu == 3: menu3() elif STATE.menu == 4: menu4() STATE.interface.update() for event in pygame.event.get(): if event.type == MOUSEBUTTONUP: pos = pygame.mouse.get_pos() # get position of touch STATE.interface.pos = pos break pygame.display.flip() clk.tick(60) # save last parameters with open("targets.txt", 'w') as f: f.write(f'humidity {STATE.targets["humidity"]}\ntemp {STATE.targets["temp"]}\nppm {STATE.targets["ppm"]}\nr {STATE.targets["r"]}\ng {STATE.targets["g"]}\nb {STATE.targets["b"]}') with open("lighting.txt", 'w') as f: print("WRITING") for b in STATE.light_schedule: f.write(str(b)) if(t0 > TIMEOUT): print("TIMED OUT") try: shutdown() except: print("A little longer...") controller.LED_STRIP.shutoff() GPIO.cleanup() def create_interface(interface_num): if interface_num == 1: return Interface1() elif interface_num == 2: return Interface2() elif interface_num == 3: return Interface3() elif interface_num == 4: return Interface4() if __name__ == "__main__": main() #-------------------graphics.py---------------- from dataclasses import dataclass import pygame @dataclass class Graphics: # Colors WHITE = 255, 255, 255 BLACK = 0, 0, 0 RED = 255, 0, 0 GREEN = 200, 90, 10 BLUE = 0, 0, 255 ORANGE = 234, 163, 50 BLUISH: tuple[Literal[45], Literal[175], Literal[220]] = 45, 175, 220 GREENISH = 45, 220, 110 def set_screen(self, screen): self.screen = screen def initialize_fonts(self): self.menu_button_font = pygame.font.Font(None, 20) self.display_font = pygame.font.Font(None, 30) self.small_font = pygame.font.Font(None,15) #-----------------------AHT20.py---------------- from smbus2 import SMBus import time def get_normalized_bit(value, bit_index): # Return only one bit from value indicated in bit_index return (value >> bit_index) & 1 AHT20_I2CADDR = 0x38 AHT20_CMD_SOFTRESET = [0xBA] AHT20_CMD_INITIALIZE = [0xBE, 0x08, 0x00] AHT20_CMD_MEASURE = [0xAC, 0x33, 0x00] AHT20_STATUSBIT_BUSY = 7 # The 7th bit is the Busy indication bit. 1 = Busy, 0 = not. AHT20_STATUSBIT_CALIBRATED = 3 # The 3rd bit is the CAL (calibration) Enable bit. 1 = Calibrated, 0 = not class AHT20: # I2C communication driver for AHT20, using only smbus2 def __init__(self, BusNum=1): # Initialize AHT20 self.BusNum = BusNum self.cmd_soft_reset() # Check for calibration, if not done then do and wait 10 ms if not self.get_status_calibrated == 1: self.cmd_initialize() while not self.get_status_calibrated() == 1: time.sleep(0.01) def cmd_soft_reset(self): # Send the command to soft reset with SMBus(self.BusNum) as i2c_bus: i2c_bus.write_i2c_block_data(AHT20_I2CADDR, 0x0, AHT20_CMD_SOFTRESET) time.sleep(0.04) # Wait 40 ms after poweron return True def cmd_initialize(self): # Send the command to initialize (calibrate) with SMBus(self.BusNum) as i2c_bus: i2c_bus.write_i2c_block_data(AHT20_I2CADDR, 0x0 , AHT20_CMD_INITIALIZE) return True def cmd_measure(self): # Send the command to measure with SMBus(self.BusNum) as i2c_bus: i2c_bus.write_i2c_block_data(AHT20_I2CADDR, 0, AHT20_CMD_MEASURE) time.sleep(0.08) # Wait 80 ms after measure return True def get_status(self): # Get the full status byte with SMBus(self.BusNum) as i2c_bus: return i2c_bus.read_i2c_block_data(AHT20_I2CADDR, 0x0, 1)[0] return True def get_status_calibrated(self): # Get the calibrated bit return get_normalized_bit(self.get_status(), AHT20_STATUSBIT_CALIBRATED) def get_status_busy(self): # Get the busy bit return get_normalized_bit(self.get_status(), AHT20_STATUSBIT_BUSY) def get_measure(self): # Get the full measure # Command a measure self.cmd_measure() # Check if busy bit = 0, otherwise wait 80 ms and retry while self.get_status_busy() == 1: time.sleep(0.08) # Wait 80 ns # Read data and return it with SMBus(self.BusNum) as i2c_bus: return i2c_bus.read_i2c_block_data(AHT20_I2CADDR, 0x0, 7) def get_temperature(self): # Get a measure, select proper bytes, return converted data measure = self.get_measure() measure = ((measure[3] & 0xF) << 16) | (measure[4] << 8) | measure[5] measure = measure / (pow(2,20))*200-50 return measure def get_humidity(self): # Get a measure, select proper bytes, return converted data measure = self.get_measure() measure = (measure[1] << 12) | (measure[2] << 4) | (measure[3] >> 4) measure = measure * 100 / pow(2,20) return measure if __name__ == "__main__": aht = AHT20(1) print(aht.get_temperature(), aht.get_humidity()) #-------------------LED.py------------------- import board import neopixel from graphics import Graphics from time import sleep import RPi.GPIO as GPIO class LED: num_pixels = 50 D_PIN = board.D21 def __init__(self, color): self.pixels = neopixel.NeoPixel(LED.D_PIN, LED.num_pixels) self.color = color #self.pixels.fill(self.color) def fill(self, color): '''color = (r,g,b)''' for i in range(LED.num_pixels): self.pixels[i] = color self.color = color def shutoff(self): for i in range(LED.num_pixels): self.pixels[i] = Graphics.BLACK def turnon(self): for i in range(LED.num_pixels): self.pixels[i] = self.color if __name__ == "__main__": #pixels.fill((0xFF, 0,0)) #led = LED((255,0,255)) n = 50 pixels = neopixel.NeoPixel(board.D21, n) for i in range(n): pixels[i] = (255,0,0) sleep(2) #pixels.deinit() #pixels[4] = (255,0,255, 0) #pixels.fill((255, 0, 255)) #pixels.fill((255,0,255)) #led.turnon() '''GPIO.setmode(GPIO.BCM) GPIO.setup(12, GPIO.OUT) GPIO.output(12, GPIO.HIGH) sleep(2) GPIO.output(12, GPIO.LOW) GPIO.cleanup() ''' #---------------------motor.py-------------------- import RPi.GPIO as GPIO import os from threading import Timer from time import sleep class Motor: def __init__(self, name, PWM_PIN, IN1, IN2 = -1, offset = 1): self.name = name self.IN1 = IN1 self.IN2 = IN2 self.PWM_PIN = PWM_PIN self.offset = offset # setup pwm GPIO.setup(PWM_PIN, GPIO.OUT) GPIO.setup(IN1, GPIO.OUT) if IN2 != -1: GPIO.setup(IN2, GPIO.OUT) self.PWM = GPIO.PWM(PWM_PIN, 50) # 50Hz pwm self.PWM.start(0) def drive(self, dc, time): """time = time to drive, if -1, drive indefinitely""" print(f"{self.name}: DRIVING") sig1 = GPIO.HIGH if self.offset == 1 else GPIO.LOW sig2 = GPIO.LOW if self.offset == 1 else GPIO.HIGH GPIO.output(self.IN1, sig1) if self.IN2 != -1: GPIO.output(self.IN2, sig2) self.PWM.ChangeDutyCycle(dc) if time != -1: t = Timer(time, Motor.stop, args=[self]) t.setDaemon(True) t.start() def setSpeed(self, dc): print(f"{self.name}: NEW SPEED {dc}") self.PWM.ChangeDutyCycle(dc) def stop(self): print(f"{self.name}: STOPPING") sig1 = GPIO.LOW if self.offset == 1 else GPIO.HIGH sig2 = GPIO.LOW if self.offset == 1 else GPIO.HIGH GPIO.output(self.IN1, sig1) if self.IN2 != -1: GPIO.output(self.IN2, sig2) self.PWM.ChangeDutyCycle(100) if __name__ == "__main__": GPIO.setmode(GPIO.BCM) # GPIO pins for the A and B motors AIN1 = 6 PUMP_PWM = 4 BIN1 = 16 FAN_PWM = 24 fan = Motor("fan", FAN_PWM, BIN1) pump = Motor("pump", PUMP_PWM, AIN1, offset=-1) fan.drive(50, 5) sleep(2) fan.setSpeed(100) sleep(5) pump.drive(100, 10) sleep(12) GPIO.cleanup() #-------------------PPM.py---------------------- import busio import digitalio import board import adafruit_mcp3xxx.mcp3008 as MCP from adafruit_mcp3xxx.analog_in import AnalogIn from statistics import median class PPM: #parameters temperature_celcius = 25 buffer_length = 50 PPM_setup = False analog_buffer = [] def __init__(self): #create SPI5 bus (SPI0 used by PiTFT) self.spi5 = busio.SPI(clock=board.D15,MISO=board.D13,MOSI=board.D14) #create CS (chip select) self.cs = digitalio.DigitalInOut(board.D26) #create MCP object self.mcp = MCP.MCP3008(self.spi5,self.cs) #create analog input channel on pin 0 self.channel0 = AnalogIn(self.mcp,MCP.P0) def read(self): #while len(analog_buffer) < buffer_length: analog_value = self.channel0.value #get sample self.analog_buffer.append(analog_value) #add sample to buffer #print('sample #{length}: {val}'.format(length=len(analog_buffer), val=analog_value)) if len(self.analog_buffer) == self.buffer_length: #once 20 samples, get median, repeat average_voltage = median(self.analog_buffer) / (2**15) #constant is arbitrary (given const didnt work) self.analog_buffer = [] #TDS value calclations compensation_coefficient = 1.0 + 0.02 * (self.temperature_celcius - 25.0) compensation_voltage = average_voltage / compensation_coefficient tds_value:float = (((133.42*compensation_voltage**3) - (255.86*compensation_voltage**2) + (857.39*compensation_voltage))*.5) return tds_value else: return -1 #----------------------lighting.txt----------------- 000000000000000000000000 #--------------------Targets.txt-------------------- humidity 40 temp 80 ppm 5 r 0 g 0 b 0